"""This file is part of uio-to-graphviz.

uio-to-graphviz is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

uio-to-graphviz is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with uio-to-graphviz. If not, see <http://www.gnu.org/licenses/>.
"""

from urllib import urlopen
#from HTMLParser import HTMLParser
import HTMLParser
import sys
import os
import re
# All urls should be a member of this class.
# If given 'u = UiOUrl("INF3400")' it will find the url: 'http://www.uio.no/studier/emner/matnat/ifi/INF3400/'
# And one can then get the full url by 'u()' or just the course code 'u.courseCode()'.
from uioUrl import UiOUrl

class MyParser(HTMLParser.HTMLParser):
    # Expects a website like this:
    # http://www.uio.no/studier/emner/matnat/ifi/INF3400/
    # and fills up the lists:
    # mandatoryRequirements # courses that are mandatory to take before INF3400
    # recommendedRequirements # courses that are recommended to take before INF3400
    # overlapPages # courses that overlap with INF3400 (can be partial overlap)
    # It also contains a boolean 'invalid' if the course is discontinued. And raises LookupError on 404.
    # TODO: mandatoryRequirements is not in use, everything is listed as recommendedRequirements.
    # See HTMLParser documentation for more information.
    def __init__(self):
        self.inMandatory = self.inRecommended = False # prerequisites
        self.inOverlap = False
        self.inUndervisning = False
        self.inTitle = False

        self.invalid = False  # course is not longer available        

        self.mandatoryRequirements = list()
        self.recommendedRequirements = list()
        self.overlapPages = list()
        HTMLParser.HTMLParser.__init__(self)       # call parent class
    
    def handle_starttag(self, tag, attr):
        if tag == 'title':
            self.inTitle = True
        else:
            self.inTitle = False
            
        if tag == 'h2':
            if 'forkunnskaper' in attr[0]: # first time
                self.inMandatory = True
            elif self.inMandatory == True: # second time
                self.inMandatory = False

            if 'overlapp' in attr[0]: # first time
                self.inOverlap = True
            elif self.inOverlap == True: # second time
                self.inOverlap = False

            if len(attr[0]) == 2 and attr[0][1] == 'statusmessage':
                self.invalid = True

            if self.inRecommended == True: # second time:
                self.inRecommended = False
        elif tag == 'h3':
            if len(attr) != 0 and attr[0][1] == 'anbefalte-forkunnskaper':
                self.inRecommended = True
                self.inMandatory = False
            elif self.inRecommended == True: # second time
                self.inRecommended = False

        if self.inRecommended or self.inMandatory:
            if len(attr) != 0 and tag == 'a':
                a = attr[0][1]
                assert attr[0][0] == 'href', attr
                if a.startswith('http://www.uio.no/studier/emner/'):
                    if self.inRecommended:
                        self.recommendedRequirements.append(UiOUrl(a))
                    if self.inMandatory:
                        self.mandatoryRequirements.append(UiOUrl(a))
        if self.inOverlap == True:
            if len(attr) != 0 and tag == 'a':
                a = attr[0][1]
                assert attr[0][0] == 'href', attr
                if a.startswith('http://www.uio.no/studier/emner/'):
                    self.overlapPages.append(UiOUrl(a))

    def handle_data(self, data):
        # Some discontinued courses write in the 'Undervises:' box which course replaces the given one.
        # (but not in the overlap section...)
        if 'Undervises:' in data:
            self.inOverlap = self.inUndervisning = True
        elif self.inUndervisning == True and '.' in data:
            self.inOverlap = self.inUndervisning = False

        if self.inTitle:
            if '404:' in data:
                raise LookupError('404 detected')
            
class Graphviz:
    # This class writes out the correct graphviz syntax to a given file
    # Call with either
    # g.recommendedRequirements(a, b) to draw blue lines for every element in 'b' to 'a'.
    # g.mandatoryRequirements(a, b)  to draw black lines for every element in 'b' to 'a'.
    # g.overlap(a, b) to create a box around every element in 'b' and 'a'.
    # Limitiation: same course can not be part of multiple boxes
    # g.discontinued(course) marks the given course as discontinued (using red letters)
    # g.pageNotFound(course) marks the given course as 404
    def __init__(self, filename):
        self.open(filename)
        
    def open(self, filename, extension=''):
        self.filename = filename + '.dot'
        self.extension = extension      # Used in convert
        self.firstCall = True           # Tells __call__ that the file should be overridden
        
    def __call__(self, s):
        # Used to write to given file
        if self.firstCall:
            self.f = open(self.filename, 'w')
            self.firstCall = False
            self.f.write('digraph G {concentrate=true\n')
        else:
            self.f = open(self.filename, 'a')
            
        self.f.write(s)
        self.f.close() # We close again so that external readers may update its view (graphviz.app is able to update the file while it is being worked on)

    def recommendedRequirements(self, a, b):
        # a: from, b: to
        for item in b:
            self('\t"{1}" -> "{0}" [color=blue];\n'.format(a.courseCode(), item.courseCode()))

    def mandatoryRequirements(self, a, b):
        # a: from, t: to
        a = a.courseCode()
        for item in b:
            self('\t"{1}" -> "{0}";\n'.format(a, item.courseCode()))

    def overlap(self, a, b):
        a = a.courseCode()
        self('\tsubgraph cluster_{0} {{\n'.format(a.replace('-', '_')))
        self('\t\t"{0}";\n'.format(a))
        for item in b:
            self('\t\t"{0}";\n'.format(item.courseCode()))
        self('\t}\n')

    def discontinued(self, course):
        self('\t"{0}" [color=red];\n'.format(course.courseCode()))

    def pageNotFound(self, course):
        self('\t"{0}" [style=filled, fillcolor=red];\n'.format(course.courseCode()))        
        
    def close(self):
        # Write the closing parenthesis and close the file.
        self('}\n')
        self.f.close()
        print 'Written {0}'.format(self.filename)
        if self.extension != '':
            self.convert()

    def convert(self):
        filename = self.filename.replace('.dot', '') # TODO: bug with file.dot.dot
        extension = self.extension
        def callSystem(command):
            print '>>', command
            ret = os.system(command)
            if ret != 0:
                print 'Command failed, exiting'
                exit(1)

        callSystem("dot -Tpdf {0}.dot -o {0}.pdf".format(filename))
        if extension == 'png':
            callSystem('convert -density 300 {0}.pdf {0}.png'.format(filename))
        elif extension == 'pdf' or extension == 'png' or extension == 'dot':
            pass # already done
        else: # lets just try convert. TODO: convert is supposed to handle over 100 different file formats, but I don't know which ones!
            callSystem('convert -density 300 {0}.pdf {0}.{1}'.format(filename, extension))
        
def read(uioUrl):
    # Read a given webpage and return a finished parse object that contains lists
    # for mandatory, recommended and overlapping courses.
    print 'Visiting', uioUrl
    page = urlopen(uioUrl())
    encoding = page.headers.getparam('charset')
    if encoding != None:                # This is encountered for pages with login window (feide)
        s = page.read().decode(encoding) # read entire page
    else:
        raise LookupError('Could not read %s' % uioUrl())
    page.close()

    p = MyParser()
    try:
        p.feed(s)                           # feed page to parser
    except HTMLParser.HTMLParseError:
        print 'Could not parse', uioUrl
        raise LookupError
    p.close()            
    return p

def findDependencies(uioUrl):
    # Find and output to graphviz dependencies and overlap for a given course
    try:
        p = read(uioUrl)
    except LookupError:
        graphviz.pageNotFound(uioUrl)
        return MyParser() # need to return a valid parse object
    
    graphviz.recommendedRequirements(uioUrl, p.recommendedRequirements) # show linked courses (arrows)
    graphviz.mandatoryRequirements(uioUrl, p.mandatoryRequirements) # show linked courses (arrows)    

    if len(p.overlapPages) != 0:
        graphviz.overlap(uioUrl, p.overlapPages) # show overlaping courses (boxed)
        # Check if any overlapping pages are discontinued
        for page in p.overlapPages:
            if read(page).invalid == True:
                graphviz.discontinued(page) # show discontinued courses (red)
    # Check if this course is discontinued
    if p.invalid == True:
        graphviz.discontinued(uioUrl)# show discontinued courses (red)

    return p

visitedPages = list()                   # prevents loops and unnecessary visits to already visited sites
graphviz = Graphviz('out')

def recursion(uioUrl, level=0):
    # Start at uioUrl and call 'recursion' for every dependency
    if level >= 100:
        print 'WARNING: max recurrsion reached at', level, uioUrl.courseCode()
        return level

    urlStriped = uioUrl.courseCode()
    if urlStriped not in visitedPages:  # Don't visit if we have been here before
        page = findDependencies(uioUrl)
        visitedPages.append(urlStriped)
        # Go through both recommended and mandatory
        for uioUrl in page.recommendedRequirements:
            level = recursion(uioUrl, level)
        for uioUrl in page.mandatoryRequirements:
            level = recursion(uioUrl, level)            

    return level

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('usage:python {0} website|courseCode'.format(sys.argv[0]))
        exit(1)

    for ix in range(1, len(sys.argv)):
        if sys.argv[ix] == '-o':
            if '.' in sys.argv[ix+1]:
                filename = sys.argv[ix+1].split('.')
                extension = filename[-1]
                graphviz.open(".".join(filename[:-1]), extension)
            else:
                graphviz.open(sys.argv[ix+1])
            del sys.argv[ix]
            del sys.argv[ix]
            break

    for website in sys.argv[1:]:
        try:
            uioUrl = UiOUrl(website)
        except KeyError:
            print 'WARNING:Sorry, but could not find course', website,
            print "Try again with the full URL."

        graphviz('\t"{0}" [shape=house];\n'.format(uioUrl.courseCode()))
        recursion(uioUrl)

    graphviz.close()
